package com.ddw.meedeeuh.presenters;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.SwingWorker;

import com.ddw.meedeeuh.MeedeeuhApp;
import com.ddw.meedeeuh.shared.Sleep;
import com.ddw.meedeeuh.shared.database.DatabaseCallback;
import com.ddw.meedeeuh.shared.events.StartMovieImportEvent;
import com.ddw.meedeeuh.shared.events.StartMovieImportEventHandler;
import com.ddw.meedeeuh.shared.movie.Movie;
import com.ddw.meedeeuh.shared.movie.database.DBMovie;
import com.ddw.meedeeuh.shared.movie.filter.MovieFilter;
import com.ddw.meedeeuh.shared.movie.filter.MovieFilterFactory;
import com.ddw.meedeeuh.shared.movie.filter.MovieFilterResult;
import com.ddw.meedeeuh.shared.movie.scrapers.MovieScraper;
import com.ddw.meedeeuh.shared.movie.scrapers.MovieScraperResult;
import com.ddw.meedeeuh.shared.movie.scrapers.MovieScraperResultType;
import com.ddw.meedeeuh.shared.movie.scrapers.local.LocalMovieScraperFactory;
import com.ddw.meedeeuh.shared.mvp.PresenterBase;
import com.ddw.meedeeuh.shared.mvp.View;

public class MovieImportPresenter extends PresenterBase<MovieImportPresenter.MovieImportView> {

    private static final Logger LOGGER = Logger.getLogger(MovieImportPresenter.class.getName());
    private MovieImportWorker worker = null;

    public MovieImportPresenter(MeedeeuhApp app, MovieImportView view) {
        super(app, view);
    }

    public static interface MovieImportView extends View {

        public void showView();

        public void hideView();

        public void setProgress(int percentage);

        public void setMessage(String message);

        public void addCloseListener(ActionListener listener);

        public void addOkListener(ActionListener listener);

        public boolean validateClose();

		public void setOkVisible(boolean visible);

		public void setCancelVisible(boolean visible);

    }

    @Override
    public void onSetup() {
        // Listen for movie import requests
        getApp().getEventBus().register(new StartMovieImportEventHandler() {

            @Override
            public void onStartMovieImport(StartMovieImportEvent e) {
                getView().setProgress(0);
                getView().setMessage("Starting import, please wait...");
                getView().setOkVisible(false);
                getView().setCancelVisible(true);

                try {
                    MovieImportPresenter.this.worker = new MovieImportWorker();
                    MovieImportPresenter.this.worker.execute();
                } catch (MovieImportWorkerException ex) {
                    LOGGER.log(Level.SEVERE, "Failed to import movies", ex);
                    getView().setProgress(0);
                    getView().setMessage(String.format("<html><font style='color:red'>%s, failed to import movies</font></html>", ex.getMessage()));
                } catch (Throwable ex) {
                    LOGGER.log(Level.SEVERE, "Failed to import movies", ex);
                    getView().setProgress(0);
                    getView().setMessage("<html><font style='color:red'>Unknown error (see log), failed to import movies</font></html>");
                }

                getView().showView();
            }

        });

        // Listen for attempts to close the dialog
        getView().addCloseListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                if(getView().validateClose()) {
                	MovieImportPresenter.this.worker.cancel(true);
                    getView().hideView();
                }
            }

        });

        // Listen for ok button
        getView().addOkListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent e) {
				getView().hideView();
			}

		});
    }

    private class MovieImportWorker extends SwingWorker<List<Movie>, String> {

        @Override
        protected List<Movie> doInBackground() throws Exception {
            LOGGER.finer("Entering the movie import routine");

            // Delete movies from database
            LOGGER.finer("Deleting movies from database");
            final Future<Boolean> databaseDelete = startDeleteMoviesFromLocalDatabase();

            publish(1, 0, "Searching for movie folders");
            final Sleep sleep = new Sleep(); // Start sleep count

            // Get the root folder
            LOGGER.finest("Getting root folder");
            final File root = new File(getApp().getMovieSettings().getLibraryPath());
            LOGGER.finer("Root Folder: " + root.getPath());

            // Check root exists
            LOGGER.finest("Checking root folder exists and is accessible");
            if(root.exists() == false) {
                throw new RuntimeException("Root folder does not exist or is not accessible");
            }
            LOGGER.finer("Root folder exists and is accessible");

            LOGGER.finest("Getting allowed movie file types");
            final List<String> allowedMovieTypes = Arrays.asList(getApp().getMovieSettings().getMovieExtensions());
            LOGGER.finer(String.format("Found %d allowed movie file types", allowedMovieTypes.size()));

            // Check allowed movie types
            if(allowedMovieTypes.size() == 0) {
                throw new RuntimeException("There were no allowed movie file types to import");
            }

            // Compile the replacement patterns, no duplicates
            LOGGER.finest("Precompiling ignore string patterns");
            final Map<String, Pattern> ignoreStrings = new HashMap<String, Pattern>(getApp().getMovieSettings().getIgnoreStrings().length);
            for(final String ignoreString : getApp().getMovieSettings().getIgnoreStrings()) {
                ignoreStrings.put(ignoreString, Pattern.compile(Matcher.quoteReplacement(ignoreString)));
            }
            LOGGER.finer(String.format("Found %d ignore strings", ignoreStrings.size()));

            // Allow UI to update if too quick then reset
            sleep.ifQuickerThan(1000, 1000);
            sleep.reset();

            // Apply the movie filter to find acceptable movie folders
            final MovieFilter movieFilter = MovieFilterFactory.create(getApp().getMovieSettings(), ignoreStrings, allowedMovieTypes);
            final List<MovieFilterResult> movieFolders = filterMovies(root, movieFilter);
            LOGGER.fine(String.format("Found %d movies to import", movieFolders.size()));

            // Allow UI to update if too quick then reset
            sleep.ifQuickerThan(1000, 1000);
            sleep.reset();

            // Scrape as much movie information from the movie sources
            final List<Movie> movies = scrapeMovies(movieFolders);
            LOGGER.fine(String.format("Scraped %d movies", movieFolders.size()));

            // Allow UI to update if too quick then reset
            sleep.ifQuickerThan(1000, 1000);
            sleep.reset();

            // Wait for database deletion to complete (if it hasn't already)
            LOGGER.finest("Waiting for database deletion to complete");
            publish(3, 100, "Waiting for database to remove all movies");
            if(databaseDelete.get() == false) {
            	throw new MovieImportWorkerException("Failed to delete movies from database");
            }
            LOGGER.finer("Database deletion complete");

            // Insert movies into database
            addMoviesToLocalDatabase(movies);

            // Perform a database tidy
            publish(4, 100, "Tidying database");
            getApp().getMovieDatabase().clean();

            // Allow UI to update if too quick then reset
            sleep.ifQuickerThan(1000, 1000);

            return movies;
        }

		@Override
        protected void process(List<String> chunks) {
            getView().setProgress(getProgress());
            getView().setMessage(chunks.get(chunks.size() - 1));
            super.process(chunks);
        }

        @Override
        protected void done() {
            if(getState() == StateValue.DONE) {
                try {
                    getView().setProgress(100);
                    getView().setOkVisible(true);
                    getView().setCancelVisible(false);
                    getView().setMessage(String.format("<html>Finished import, %d movies imported</html>", get().size()));
                } catch (InterruptedException e) {
                	LOGGER.log(Level.SEVERE, "Unknown error occurred (interrupted exception) importing movies", e);
                } catch (ExecutionException e) {
                	LOGGER.log(Level.SEVERE, "Unknown error occurred (execution exception) importing movies", e);
                } catch (CancellationException e) {
                    LOGGER.info("Import was cancelled");
                }
            }

            super.done();
        }

        private int getPercentage(int i, int total) {
            final double percent = (((double)i + 1.0) / (double)total) * 100.0;
            return Math.min((int)Math.round(percent), 100);
        }

		private Future<Boolean> startDeleteMoviesFromLocalDatabase() {
			return getApp().getMovieDatabase().deleteMovies(new DatabaseCallback<Boolean>() {

				@Override
				public void onSuccess(Boolean result) {
					LOGGER.finest("Deleted movies from database");
				}

				@Override
				public void onFailure(Throwable e) {
					throw new MovieImportWorkerException("Failed to delete movies from database", e);
				}

			});
		}

        private void addMoviesToLocalDatabase(List<Movie> movies) throws MovieImportWorkerException {
        	for(int i = 0, s = movies.size(); i < s; i++) {
        		publish(4, getPercentage(i, s), String.format("Adding movies to database, %d of %d", i + 1, s));

        		// Save the movie
        		final Movie movie = movies.get(i);
        		DBMovie savedMovie = null;
        		try {
        			savedMovie = getApp().getMovieDatabase().setMovie(movie, new DatabaseCallback<DBMovie>() {

						@Override
						public void onSuccess(DBMovie result) {}

						@Override
						public void onFailure(Throwable e) {
							LOGGER.log(Level.SEVERE, String.format("There was an error saving the movie: %s", movie.getName()), e);
						}

					}).get();

        			if(savedMovie == null) {
        				throw new RuntimeException("There was an error saving the movie");
        			}
				} catch (Throwable e) {
					throw new MovieImportWorkerException(String.format("Failed to save movie: %s", movie.getName()), e);
				}

				// Otherwise save was successful
				LOGGER.finer(String.format("The movie id: %d, movie name: %s was saved to the database successfully", savedMovie.getMovieId(), savedMovie.getName()));
        	}
		}

        private List<Movie> scrapeMovies(List<MovieFilterResult> movieFolders) {
            final List<Movie> movies = new ArrayList<Movie>(movieFolders.size());

            // Loop through and scrape files
            for(int i = 0, s = movieFolders.size(); i < s; i++) {
            	publish(3, getPercentage(i, s), String.format("Importing movies, %d of %d", i + 1, s));

                final MovieFilterResult movieFolder = movieFolders.get(i);
                final MovieScraper scraper = LocalMovieScraperFactory.create(movieFolder.getMovieFolderDirectories(), movieFolder.getMovieFolderFiles());
                final MovieScraperResult result = scraper.scrape(getApp().getMovieSettings(), movieFolder.getMovieFolder(), movieFolder.getMovieFiles());

                if(result.getResult() == MovieScraperResultType.SUCCESS) {
                    movies.add(result.getMovie());
                }
            }

            return movies;
        }

        private List<MovieFilterResult> filterMovies(File root, MovieFilter movieFilter) {
            LOGGER.finest("Getting all children of root folder");
            final File[] rootFiles = root.listFiles();
            final List<MovieFilterResult> movies = new ArrayList<MovieFilterResult>();

            // Loop through and filter the files
            for(int i = 0, s = rootFiles.length; i < s; i++) {
            	publish(2, getPercentage(i, s), String.format("Filtering movie folders, %d of %d", i + 1, s));

                final File movieFolder = rootFiles[i];
                final MovieFilterResult result = movieFilter.filter(movieFolder);

                // Only add acceptible results
                if(result.isAcceptible()) {
                    movies.add(result);
                }
            }

            return movies;
        }

        private void publish(int step, int progress, String message) {
        	setProgress(progress);
        	publish(String.format("<html><font style='color:#888888'>Step %d of 4:</font> %s</html>", step, message));
        }

    }

    @SuppressWarnings("serial")
	private static class MovieImportWorkerException extends RuntimeException {

    	public MovieImportWorkerException(String message, Throwable e) {
    		super(message, e);
    	}

    	public MovieImportWorkerException(String message) {
    		super(message);
    	}

    }

}
